/*!
 * skrollr core
 *
 * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr
 *
 * Free to use under terms of MIT license
 */
(function(window, document, undefined) {
  "use strict";

  /*
	 * Global api.
	 */
  var skrollr = (window.skrollr = {
    get: function() {
      return _instance;
    },
    //Main entry point.
    init: function(options) {
      return _instance || new Skrollr(options);
    },
    VERSION: "0.6.21"
  });

  //Minify optimization.
  var hasProp = Object.prototype.hasOwnProperty;
  var Math = window.Math;
  var getStyle = window.getComputedStyle;

  //They will be filled when skrollr gets initialized.
  var documentElement;
  var body;

  var EVENT_TOUCHSTART = "touchstart";
  var EVENT_TOUCHMOVE = "touchmove";
  var EVENT_TOUCHCANCEL = "touchcancel";
  var EVENT_TOUCHEND = "touchend";

  var SKROLLABLE_CLASS = "skrollable";
  var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + "-before";
  var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + "-between";
  var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + "-after";

  var SKROLLR_CLASS = "skrollr";
  var NO_SKROLLR_CLASS = "no-" + SKROLLR_CLASS;
  var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + "-desktop";
  var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + "-mobile";

  var DEFAULT_EASING = "linear";
  var DEFAULT_DURATION = 1000; //ms
  var DEFAULT_MOBILE_DECELERATION = 0.004; //pixel/ms²

  var DEFAULT_SMOOTH_SCROLLING_DURATION = 200; //ms

  var ANCHOR_START = "start";
  var ANCHOR_END = "end";
  var ANCHOR_CENTER = "center";
  var ANCHOR_BOTTOM = "bottom";

  //The property which will be added to the DOM element to hold the ID of the skrollable.
  var SKROLLABLE_ID_DOM_PROPERTY = "___skrollable_id";

  var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;

  var rxTrim = /^\s+|\s+$/g;

  //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].
  var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;

  var rxPropValue = /\s*([\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;

  //Easing function names follow the property in square brackets.
  var rxPropEasing = /^([a-z\-]+)\[(\w+)\]$/;

  var rxCamelCase = /-([a-z])/g;
  var rxCamelCaseFn = function(str, letter) {
    return letter.toUpperCase();
  };

  //Numeric values with optional sign.
  var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;

  //Used to replace occurences of {?} with a number.
  var rxInterpolateString = /\{\?\}/g;

  //Finds rgb(a) colors, which don't use the percentage notation.
  var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;

  //Finds all gradients.
  var rxGradient = /[a-z\-]+-gradient/g;

  //Vendor prefix. Will be set once skrollr gets initialized.
  var theCSSPrefix = "";
  var theDashedCSSPrefix = "";

  //Will be called once (when skrollr gets initialized).
  var detectCSSPrefix = function() {
    //Only relevant prefixes. May be extended.
    //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
    var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;

    //Detect prefix for current browser by finding the first property using a prefix.
    if (!getStyle) {
      return;
    }

    var style = getStyle(body, null);

    for (var k in style) {
      //We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy.
      theCSSPrefix =
        k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes));

      if (theCSSPrefix) {
        break;
      }
    }

    //Did we even detect a prefix?
    if (!theCSSPrefix) {
      theCSSPrefix = theDashedCSSPrefix = "";

      return;
    }

    theCSSPrefix = theCSSPrefix[0];

    //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
    if (theCSSPrefix.slice(0, 1) === "-") {
      theDashedCSSPrefix = theCSSPrefix;

      //There's no logic behind these. Need a look up.
      theCSSPrefix = {
        "-webkit-": "webkit",
        "-moz-": "Moz",
        "-ms-": "ms",
        "-o-": "O"
      }[theCSSPrefix];
    } else {
      theDashedCSSPrefix = "-" + theCSSPrefix.toLowerCase() + "-";
    }
  };

  var polyfillRAF = function() {
    var requestAnimFrame =
      window.requestAnimationFrame ||
      window[theCSSPrefix.toLowerCase() + "RequestAnimationFrame"];

    var lastTime = _now();

    if (_isMobile || !requestAnimFrame) {
      requestAnimFrame = function(callback) {
        //How long did it take to render?
        var deltaTime = _now() - lastTime;
        var delay = Math.max(0, 1000 / 60 - deltaTime);

        return window.setTimeout(function() {
          lastTime = _now();
          callback();
        }, delay);
      };
    }

    return requestAnimFrame;
  };

  var polyfillCAF = function() {
    var cancelAnimFrame =
      window.cancelAnimationFrame ||
      window[theCSSPrefix.toLowerCase() + "CancelAnimationFrame"];

    if (_isMobile || !cancelAnimFrame) {
      cancelAnimFrame = function(timeout) {
        return window.clearTimeout(timeout);
      };
    }

    return cancelAnimFrame;
  };

  //Built-in easing functions.
  var easings = {
    begin: function() {
      return 0;
    },
    end: function() {
      return 1;
    },
    linear: function(p) {
      return p;
    },
    quadratic: function(p) {
      return p * p;
    },
    cubic: function(p) {
      return p * p * p;
    },
    swing: function(p) {
      return -Math.cos(p * Math.PI) / 2 + 0.5;
    },
    sqrt: function(p) {
      return Math.sqrt(p);
    },
    outCubic: function(p) {
      return Math.pow(p - 1, 3) + 1;
    },
    //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this
    bounce: function(p) {
      var a;

      if (p <= 0.5083) {
        a = 3;
      } else if (p <= 0.8489) {
        a = 9;
      } else if (p <= 0.96208) {
        a = 27;
      } else if (p <= 0.99981) {
        a = 91;
      } else {
        return 1;
      }

      return 1 - Math.abs((3 * Math.cos(p * a * 1.028)) / a);
    }
  };

  /**
   * Constructor.
   */
  function Skrollr(options) {
    documentElement = document.documentElement;
    body = document.body;

    detectCSSPrefix();

    _instance = this;

    options = options || {};

    _constants = options.constants || {};

    //We allow defining custom easings or overwrite existing.
    if (options.easing) {
      for (var e in options.easing) {
        easings[e] = options.easing[e];
      }
    }

    _edgeStrategy = options.edgeStrategy || "set";

    _listeners = {
      //Function to be called right before rendering.
      beforerender: options.beforerender,

      //Function to be called right after finishing rendering.
      render: options.render
    };

    //forceHeight is true by default
    _forceHeight = options.forceHeight !== false;

    if (_forceHeight) {
      _scale = options.scale || 1;
    }

    _mobileDeceleration =
      options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;

    _smoothScrollingEnabled = options.smoothScrolling !== false;
    _smoothScrollingDuration =
      options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;

    //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.
    _smoothScrolling = {
      targetTop: _instance.getScrollTop()
    };

    //A custom check function may be passed.
    _isMobile = (options.mobileCheck ||
      function() {
        return /Android|iPhone|iPad|iPod|BlackBerry/i.test(
          navigator.userAgent || navigator.vendor || window.opera
        );
      })();

    if (_isMobile) {
      _skrollrBody = document.getElementById("skrollr-body");

      //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).
      if (_skrollrBody) {
        _detect3DTransforms();
      }

      _initMobile();
      _updateClass(
        documentElement,
        [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS],
        [NO_SKROLLR_CLASS]
      );
    } else {
      _updateClass(
        documentElement,
        [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS],
        [NO_SKROLLR_CLASS]
      );
    }

    //Triggers parsing of elements and a first reflow.
    _instance.refresh();

    _addEvent(window, "resize orientationchange", function() {
      var width = documentElement.clientWidth;
      var height = documentElement.clientHeight;

      //Only reflow if the size actually changed (#271).
      if (height !== _lastViewportHeight || width !== _lastViewportWidth) {
        _lastViewportHeight = height;
        _lastViewportWidth = width;

        _requestReflow = true;
      }
    });

    var requestAnimFrame = polyfillRAF();

    //Let's go.
    (function animloop() {
      _render();
      _animFrame = requestAnimFrame(animloop);
    })();

    return _instance;
  }

  /**
   * (Re)parses some or all elements.
   */
  Skrollr.prototype.refresh = function(elements) {
    var elementIndex;
    var elementsLength;
    var ignoreID = false;

    //Completely reparse anything without argument.
    if (elements === undefined) {
      //Ignore that some elements may already have a skrollable ID.
      ignoreID = true;

      _skrollables = [];
      _skrollableIdCounter = 0;

      elements = document.getElementsByTagName("*");
    } else {
      //We accept a single element or an array of elements.
      elements = [].concat(elements);
    }

    elementIndex = 0;
    elementsLength = elements.length;

    for (; elementIndex < elementsLength; elementIndex++) {
      var el = elements[elementIndex];
      var anchorTarget = el;
      var keyFrames = [];

      //If this particular element should be smooth scrolled.
      var smoothScrollThis = _smoothScrollingEnabled;

      //The edge strategy for this particular element.
      var edgeStrategy = _edgeStrategy;

      if (!el.attributes) {
        continue;
      }

      //Iterate over all attributes and search for key frame attributes.
      var attributeIndex = 0;
      var attributesLength = el.attributes.length;

      for (; attributeIndex < attributesLength; attributeIndex++) {
        var attr = el.attributes[attributeIndex];

        if (attr.name === "data-anchor-target") {
          anchorTarget = document.querySelector(attr.value);

          if (anchorTarget === null) {
            throw 'Unable to find anchor target "' + attr.value + '"';
          }

          continue;
        }

        //Global smooth scrolling can be overridden by the element attribute.
        if (attr.name === "data-smooth-scrolling") {
          smoothScrollThis = attr.value !== "off";

          continue;
        }

        //Global edge strategy can be overridden by the element attribute.
        if (attr.name === "data-edge-strategy") {
          edgeStrategy = attr.value;

          continue;
        }

        var match = attr.name.match(rxKeyframeAttribute);

        if (match === null) {
          continue;
        }

        var kf = {
          props: attr.value,
          //Point back to the element as well.
          element: el
        };

        keyFrames.push(kf);

        var constant = match[1];

        if (constant) {
          //Strip the underscore prefix.
          kf.constant = constant.substr(1);
        }

        //Get the key frame offset.
        var offset = match[2];

        //Is it a percentage offset?
        if (/p$/.test(offset)) {
          kf.isPercentage = true;
          kf.offset = (offset.slice(0, -1) | 0) / 100;
        } else {
          kf.offset = offset | 0;
        }

        var anchor1 = match[3];

        //If second anchor is not set, the first will be taken for both.
        var anchor2 = match[4] || anchor1;

        //"absolute" (or "classic") mode, where numbers mean absolute scroll offset.
        if (!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {
          kf.mode = "absolute";

          //data-end needs to be calculated after all key frames are known.
          if (anchor1 === ANCHOR_END) {
            kf.isEnd = true;
          } else if (!kf.isPercentage) {
            //For data-start we can already set the key frame w/o calculations.
            //#59: "scale" options should only affect absolute mode.
            kf.offset = kf.offset * _scale;
          }
        }
        //"relative" mode, where numbers are relative to anchors.
        else {
          kf.mode = "relative";
          kf.anchors = [anchor1, anchor2];
        }
      }

      //Does this element have key frames?
      if (!keyFrames.length) {
        continue;
      }

      //Will hold the original style and class attributes before we controlled the element (see #80).
      var styleAttr, classAttr;

      var id;

      if (!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
        //We already have this element under control. Grab the corresponding skrollable id.
        id = el[SKROLLABLE_ID_DOM_PROPERTY];
        styleAttr = _skrollables[id].styleAttr;
        classAttr = _skrollables[id].classAttr;
      } else {
        //It's an unknown element. Asign it a new skrollable id.
        id = el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++;
        styleAttr = el.style.cssText;
        classAttr = _getClass(el);
      }

      _skrollables[id] = {
        element: el,
        styleAttr: styleAttr,
        classAttr: classAttr,
        anchorTarget: anchorTarget,
        keyFrames: keyFrames,
        smoothScrolling: smoothScrollThis,
        edgeStrategy: edgeStrategy
      };

      _updateClass(el, [SKROLLABLE_CLASS], []);
    }

    //Reflow for the first time.
    _reflow();

    //Now that we got all key frame numbers right, actually parse the properties.
    elementIndex = 0;
    elementsLength = elements.length;

    for (; elementIndex < elementsLength; elementIndex++) {
      var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];

      if (sk === undefined) {
        continue;
      }

      //Parse the property string to objects
      _parseProps(sk);

      //Fill key frames with missing properties from left and right
      _fillProps(sk);
    }

    return _instance;
  };

  /**
   * Transform "relative" mode to "absolute" mode.
   * That is, calculate anchor position and offset of element.
   */
  Skrollr.prototype.relativeToAbsolute = function(
    element,
    viewportAnchor,
    elementAnchor
  ) {
    var viewportHeight = documentElement.clientHeight;
    var box = element.getBoundingClientRect();
    var absolute = box.top;

    //#100: IE doesn't supply "height" with getBoundingClientRect.
    var boxHeight = box.bottom - box.top;

    if (viewportAnchor === ANCHOR_BOTTOM) {
      absolute -= viewportHeight;
    } else if (viewportAnchor === ANCHOR_CENTER) {
      absolute -= viewportHeight / 2;
    }

    if (elementAnchor === ANCHOR_BOTTOM) {
      absolute += boxHeight;
    } else if (elementAnchor === ANCHOR_CENTER) {
      absolute += boxHeight / 2;
    }

    //Compensate scrolling since getBoundingClientRect is relative to viewport.
    absolute += _instance.getScrollTop();

    return (absolute + 0.5) | 0;
  };

  /**
   * Animates scroll top to new position.
   */
  Skrollr.prototype.animateTo = function(top, options) {
    options = options || {};

    var now = _now();
    var scrollTop = _instance.getScrollTop();

    //Setting this to a new value will automatically cause the current animation to stop, if any.
    _scrollAnimation = {
      startTop: scrollTop,
      topDiff: top - scrollTop,
      targetTop: top,
      duration: options.duration || DEFAULT_DURATION,
      startTime: now,
      endTime: now + (options.duration || DEFAULT_DURATION),
      easing: easings[options.easing || DEFAULT_EASING],
      done: options.done
    };

    //Don't queue the animation if there's nothing to animate.
    if (!_scrollAnimation.topDiff) {
      if (_scrollAnimation.done) {
        _scrollAnimation.done.call(_instance, false);
      }

      _scrollAnimation = undefined;
    }

    return _instance;
  };

  /**
   * Stops animateTo animation.
   */
  Skrollr.prototype.stopAnimateTo = function() {
    if (_scrollAnimation && _scrollAnimation.done) {
      _scrollAnimation.done.call(_instance, true);
    }

    _scrollAnimation = undefined;
  };

  /**
   * Returns if an animation caused by animateTo is currently running.
   */
  Skrollr.prototype.isAnimatingTo = function() {
    return !!_scrollAnimation;
  };

  Skrollr.prototype.setScrollTop = function(top, force) {
    _forceRender = force === true;

    if (_isMobile) {
      _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame);
    } else {
      window.scrollTo(0, top);
    }

    return _instance;
  };

  Skrollr.prototype.getScrollTop = function() {
    if (_isMobile) {
      return _mobileOffset;
    } else {
      return (
        window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0
      );
    }
  };

  Skrollr.prototype.getMaxScrollTop = function() {
    return _maxKeyFrame;
  };

  Skrollr.prototype.on = function(name, fn) {
    _listeners[name] = fn;

    return _instance;
  };

  Skrollr.prototype.off = function(name) {
    delete _listeners[name];

    return _instance;
  };

  Skrollr.prototype.destroy = function() {
    var cancelAnimFrame = polyfillCAF();
    cancelAnimFrame(_animFrame);
    _removeAllEvents();

    _updateClass(
      documentElement,
      [NO_SKROLLR_CLASS],
      [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]
    );

    var skrollableIndex = 0;
    var skrollablesLength = _skrollables.length;

    for (; skrollableIndex < skrollablesLength; skrollableIndex++) {
      _reset(_skrollables[skrollableIndex].element);
    }

    documentElement.style.overflow = body.style.overflow = "auto";
    documentElement.style.height = body.style.height = "auto";

    if (_skrollrBody) {
      skrollr.setStyle(_skrollrBody, "transform", "none");
    }

    _instance = undefined;
    _skrollrBody = undefined;
    _listeners = undefined;
    _forceHeight = undefined;
    _maxKeyFrame = 0;
    _scale = 1;
    _constants = undefined;
    _mobileDeceleration = undefined;
    _direction = "down";
    _lastTop = -1;
    _lastViewportWidth = 0;
    _lastViewportHeight = 0;
    _requestReflow = false;
    _scrollAnimation = undefined;
    _smoothScrollingEnabled = undefined;
    _smoothScrollingDuration = undefined;
    _smoothScrolling = undefined;
    _forceRender = undefined;
    _skrollableIdCounter = 0;
    _edgeStrategy = undefined;
    _isMobile = false;
    _mobileOffset = 0;
    _translateZ = undefined;
  };

  /*
		Private methods.
	*/

  var _initMobile = function() {
    var initialElement;
    var initialTouchY;
    var initialTouchX;
    var currentElement;
    var currentTouchY;
    var currentTouchX;
    var lastTouchY;
    var deltaY;

    var initialTouchTime;
    var currentTouchTime;
    var lastTouchTime;
    var deltaTime;

    _addEvent(
      documentElement,
      [
        EVENT_TOUCHSTART,
        EVENT_TOUCHMOVE,
        EVENT_TOUCHCANCEL,
        EVENT_TOUCHEND
      ].join(" "),
      function(e) {
        var touch = e.changedTouches[0];

        currentElement = e.target;

        //We don't want text nodes.
        while (currentElement.nodeType === 3) {
          currentElement = currentElement.parentNode;
        }

        currentTouchY = touch.clientY;
        currentTouchX = touch.clientX;
        currentTouchTime = e.timeStamp;

        if (!rxTouchIgnoreTags.test(currentElement.tagName)) {
          e.preventDefault();
        }

        switch (e.type) {
          case EVENT_TOUCHSTART:
            //The last element we tapped on.
            if (initialElement) {
              initialElement.blur();
            }

            _instance.stopAnimateTo();

            initialElement = currentElement;

            initialTouchY = lastTouchY = currentTouchY;
            initialTouchX = currentTouchX;
            initialTouchTime = currentTouchTime;

            break;
          case EVENT_TOUCHMOVE:
            //Prevent default event on touchIgnore elements in case they don't have focus yet.
            if (
              rxTouchIgnoreTags.test(currentElement.tagName) &&
              document.activeElement !== currentElement
            ) {
              e.preventDefault();
            }

            deltaY = currentTouchY - lastTouchY;
            deltaTime = currentTouchTime - lastTouchTime;

            _instance.setScrollTop(_mobileOffset - deltaY, true);

            lastTouchY = currentTouchY;
            lastTouchTime = currentTouchTime;
            break;
          default:
          case EVENT_TOUCHCANCEL:
          case EVENT_TOUCHEND:
            var distanceY = initialTouchY - currentTouchY;
            var distanceX = initialTouchX - currentTouchX;
            var distance2 = distanceX * distanceX + distanceY * distanceY;

            //Check if it was more like a tap (moved less than 7px).
            if (distance2 < 49) {
              if (!rxTouchIgnoreTags.test(initialElement.tagName)) {
                initialElement.focus();

                //It was a tap, click the element.
                var clickEvent = document.createEvent("MouseEvents");
                clickEvent.initMouseEvent(
                  "click",
                  true,
                  true,
                  e.view,
                  1,
                  touch.screenX,
                  touch.screenY,
                  touch.clientX,
                  touch.clientY,
                  e.ctrlKey,
                  e.altKey,
                  e.shiftKey,
                  e.metaKey,
                  0,
                  null
                );
                initialElement.dispatchEvent(clickEvent);
              }

              return;
            }

            initialElement = undefined;

            var speed = deltaY / deltaTime;

            //Cap speed at 3 pixel/ms.
            speed = Math.max(Math.min(speed, 3), -3);

            var duration = Math.abs(speed / _mobileDeceleration);
            var targetOffset =
              speed * duration +
              0.5 * _mobileDeceleration * duration * duration;
            var targetTop = _instance.getScrollTop() - targetOffset;

            //Relative duration change for when scrolling above bounds.
            var targetRatio = 0;

            //Change duration proportionally when scrolling would leave bounds.
            if (targetTop > _maxKeyFrame) {
              targetRatio = (_maxKeyFrame - targetTop) / targetOffset;

              targetTop = _maxKeyFrame;
            } else if (targetTop < 0) {
              targetRatio = -targetTop / targetOffset;

              targetTop = 0;
            }

            duration = duration * (1 - targetRatio);

            _instance.animateTo((targetTop + 0.5) | 0, {
              easing: "outCubic",
              duration: duration
            });
            break;
        }
      }
    );

    //Just in case there has already been some native scrolling, reset it.
    window.scrollTo(0, 0);
    documentElement.style.overflow = body.style.overflow = "hidden";
  };

  /**
   * Updates key frames which depend on others / need to be updated on resize.
   * That is "end" in "absolute" mode and all key frames in "relative" mode.
   * Also handles constants, because they may change on resize.
   */
  var _updateDependentKeyFrames = function() {
    var viewportHeight = documentElement.clientHeight;
    var processedConstants = _processConstants();
    var skrollable;
    var element;
    var anchorTarget;
    var keyFrames;
    var keyFrameIndex;
    var keyFramesLength;
    var kf;
    var skrollableIndex;
    var skrollablesLength;
    var offset;
    var constantValue;

    //First process all relative-mode elements and find the max key frame.
    skrollableIndex = 0;
    skrollablesLength = _skrollables.length;

    for (; skrollableIndex < skrollablesLength; skrollableIndex++) {
      skrollable = _skrollables[skrollableIndex];
      element = skrollable.element;
      anchorTarget = skrollable.anchorTarget;
      keyFrames = skrollable.keyFrames;

      keyFrameIndex = 0;
      keyFramesLength = keyFrames.length;

      for (; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
        kf = keyFrames[keyFrameIndex];

        offset = kf.offset;
        constantValue = processedConstants[kf.constant] || 0;

        kf.frame = offset;

        if (kf.isPercentage) {
          //Convert the offset to percentage of the viewport height.
          offset = offset * viewportHeight;

          //Absolute + percentage mode.
          kf.frame = offset;
        }

        if (kf.mode === "relative") {
          _reset(element);

          kf.frame =
            _instance.relativeToAbsolute(
              anchorTarget,
              kf.anchors[0],
              kf.anchors[1]
            ) - offset;

          _reset(element, true);
        }

        kf.frame += constantValue;

        //Only search for max key frame when forceHeight is enabled.
        if (_forceHeight) {
          //Find the max key frame, but don't use one of the data-end ones for comparison.
          if (!kf.isEnd && kf.frame > _maxKeyFrame) {
            _maxKeyFrame = kf.frame;
          }
        }
      }
    }

    //#133: The document can be larger than the maxKeyFrame we found.
    _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight());

    //Now process all data-end keyframes.
    skrollableIndex = 0;
    skrollablesLength = _skrollables.length;

    for (; skrollableIndex < skrollablesLength; skrollableIndex++) {
      skrollable = _skrollables[skrollableIndex];
      keyFrames = skrollable.keyFrames;

      keyFrameIndex = 0;
      keyFramesLength = keyFrames.length;

      for (; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
        kf = keyFrames[keyFrameIndex];

        constantValue = processedConstants[kf.constant] || 0;

        if (kf.isEnd) {
          kf.frame = _maxKeyFrame - kf.offset + constantValue;
        }
      }

      skrollable.keyFrames.sort(_keyFrameComparator);
    }
  };

  /**
   * Calculates and sets the style properties for the element at the given frame.
   * @param fakeFrame The frame to render at when smooth scrolling is enabled.
   * @param actualFrame The actual frame we are at.
   */
  var _calcSteps = function(fakeFrame, actualFrame) {
    //Iterate over all skrollables.
    var skrollableIndex = 0;
    var skrollablesLength = _skrollables.length;

    for (; skrollableIndex < skrollablesLength; skrollableIndex++) {
      var skrollable = _skrollables[skrollableIndex];
      var element = skrollable.element;
      var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame;
      var frames = skrollable.keyFrames;
      var firstFrame = frames[0].frame;
      var lastFrame = frames[frames.length - 1].frame;
      var beforeFirst = frame < firstFrame;
      var afterLast = frame > lastFrame;
      var firstOrLastFrame = frames[beforeFirst ? 0 : frames.length - 1];
      var key;
      var value;

      //If we are before/after the first/last frame, set the styles according to the given edge strategy.
      if (beforeFirst || afterLast) {
        //Check if we already handled this edge case last time.
        //Note: using setScrollTop it's possible that we jumped from one edge to the other.
        if (
          (beforeFirst && skrollable.edge === -1) ||
          (afterLast && skrollable.edge === 1)
        ) {
          continue;
        }

        //Add the skrollr-before or -after class.
        _updateClass(
          element,
          [beforeFirst ? SKROLLABLE_BEFORE_CLASS : SKROLLABLE_AFTER_CLASS],
          [
            SKROLLABLE_BEFORE_CLASS,
            SKROLLABLE_BETWEEN_CLASS,
            SKROLLABLE_AFTER_CLASS
          ]
        );

        //Remember that we handled the edge case (before/after the first/last keyframe).
        skrollable.edge = beforeFirst ? -1 : 1;

        switch (skrollable.edgeStrategy) {
          case "reset":
            _reset(element);
            continue;
          case "ease":
            //Handle this case like it would be exactly at first/last keyframe and just pass it on.
            frame = firstOrLastFrame.frame;
            break;
          default:
          case "set":
            var props = firstOrLastFrame.props;

            for (key in props) {
              if (hasProp.call(props, key)) {
                value = _interpolateString(props[key].value);

                skrollr.setStyle(element, key, value);
              }
            }

            continue;
        }
      } else {
        //Did we handle an edge last time?
        if (skrollable.edge !== 0) {
          _updateClass(
            element,
            [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS],
            [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]
          );
          skrollable.edge = 0;
        }
      }

      //Find out between which two key frames we are right now.
      var keyFrameIndex = 0;
      var framesLength = frames.length - 1;

      for (; keyFrameIndex < framesLength; keyFrameIndex++) {
        if (
          frame >= frames[keyFrameIndex].frame &&
          frame <= frames[keyFrameIndex + 1].frame
        ) {
          var left = frames[keyFrameIndex];
          var right = frames[keyFrameIndex + 1];

          for (key in left.props) {
            if (hasProp.call(left.props, key)) {
              var progress = (frame - left.frame) / (right.frame - left.frame);

              //Transform the current progress using the given easing function.
              progress = left.props[key].easing(progress);

              //Interpolate between the two values
              value = _calcInterpolation(
                left.props[key].value,
                right.props[key].value,
                progress
              );

              value = _interpolateString(value);

              skrollr.setStyle(element, key, value);
            }
          }

          break;
        }
      }
    }
  };

  /**
   * Renders all elements.
   */
  var _render = function() {
    if (_requestReflow) {
      _requestReflow = false;
      _reflow();
    }

    //We may render something else than the actual scrollbar position.
    var renderTop = _instance.getScrollTop();

    //If there's an animation, which ends in current render call, call the callback after rendering.
    var afterAnimationCallback;
    var now = _now();
    var progress;

    //Before actually rendering handle the scroll animation, if any.
    if (_scrollAnimation) {
      //It's over
      if (now >= _scrollAnimation.endTime) {
        renderTop = _scrollAnimation.targetTop;
        afterAnimationCallback = _scrollAnimation.done;
        _scrollAnimation = undefined;
      } else {
        //Map the current progress to the new progress using given easing function.
        progress = _scrollAnimation.easing(
          (now - _scrollAnimation.startTime) / _scrollAnimation.duration
        );

        renderTop =
          (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0;
      }

      _instance.setScrollTop(renderTop, true);
    }
    //Smooth scrolling only if there's no animation running and if we're not forcing the rendering.
    else if (!_forceRender) {
      var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop;

      //The user scrolled, start new smooth scrolling.
      if (smoothScrollingDiff) {
        _smoothScrolling = {
          startTop: _lastTop,
          topDiff: renderTop - _lastTop,
          targetTop: renderTop,
          startTime: _lastRenderCall,
          endTime: _lastRenderCall + _smoothScrollingDuration
        };
      }

      //Interpolate the internal scroll position (not the actual scrollbar).
      if (now <= _smoothScrolling.endTime) {
        //Map the current progress to the new progress using easing function.
        progress = easings.sqrt(
          (now - _smoothScrolling.startTime) / _smoothScrollingDuration
        );

        renderTop =
          (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0;
      }
    }

    //That's were we actually "scroll" on mobile.
    if (_isMobile && _skrollrBody) {
      //Set the transform ("scroll it").
      skrollr.setStyle(
        _skrollrBody,
        "transform",
        "translate(0, " + -_mobileOffset + "px) " + _translateZ
      );
    }

    //Did the scroll position even change?
    if (_forceRender || _lastTop !== renderTop) {
      //Remember in which direction are we scrolling?
      _direction =
        renderTop > _lastTop
          ? "down"
          : renderTop < _lastTop
            ? "up"
            : _direction;

      _forceRender = false;

      var listenerParams = {
        curTop: renderTop,
        lastTop: _lastTop,
        maxTop: _maxKeyFrame,
        direction: _direction
      };

      //Tell the listener we are about to render.
      var continueRendering =
        _listeners.beforerender &&
        _listeners.beforerender.call(_instance, listenerParams);

      //The beforerender listener function is able the cancel rendering.
      if (continueRendering !== false) {
        //Now actually interpolate all the styles.
        _calcSteps(renderTop, _instance.getScrollTop());

        //Remember when we last rendered.
        _lastTop = renderTop;

        if (_listeners.render) {
          _listeners.render.call(_instance, listenerParams);
        }
      }

      if (afterAnimationCallback) {
        afterAnimationCallback.call(_instance, false);
      }
    }

    _lastRenderCall = now;
  };

  /**
   * Parses the properties for each key frame of the given skrollable.
   */
  var _parseProps = function(skrollable) {
    //Iterate over all key frames
    var keyFrameIndex = 0;
    var keyFramesLength = skrollable.keyFrames.length;

    for (; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
      var frame = skrollable.keyFrames[keyFrameIndex];
      var easing;
      var value;
      var prop;
      var props = {};

      var match;

      while ((match = rxPropValue.exec(frame.props)) !== null) {
        prop = match[1];
        value = match[2];

        easing = prop.match(rxPropEasing);

        //Is there an easing specified for this prop?
        if (easing !== null) {
          prop = easing[1];
          easing = easing[2];
        } else {
          easing = DEFAULT_EASING;
        }

        //Exclamation point at first position forces the value to be taken literal.
        value = value.indexOf("!") ? _parseProp(value) : [value.slice(1)];

        //Save the prop for this key frame with his value and easing function
        props[prop] = {
          value: value,
          easing: easings[easing]
        };
      }

      frame.props = props;
    }
  };

  /**
   * Parses a value extracting numeric values and generating a format string
   * for later interpolation of the new values in old string.
   *
   * @param val The CSS value to be parsed.
   * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7]
   * where the first element is the format string later used
   * and all following elements are the numeric value.
   */
  var _parseProp = function(val) {
    var numbers = [];

    //One special case, where floats don't work.
    //We replace all occurences of rgba colors
    //which don't use percentage notation with the percentage notation.
    rxRGBAIntegerColor.lastIndex = 0;
    val = val.replace(rxRGBAIntegerColor, function(rgba) {
      return rgba.replace(rxNumericValue, function(n) {
        return (n / 255) * 100 + "%";
      });
    });

    //Handle prefixing of "gradient" values.
    //For now only the prefixed value will be set. Unprefixed isn't supported anyway.
    if (theDashedCSSPrefix) {
      rxGradient.lastIndex = 0;
      val = val.replace(rxGradient, function(s) {
        return theDashedCSSPrefix + s;
      });
    }

    //Now parse ANY number inside this string and create a format string.
    val = val.replace(rxNumericValue, function(n) {
      numbers.push(+n);
      return "{?}";
    });

    //Add the formatstring as first value.
    numbers.unshift(val);

    return numbers;
  };

  /**
   * Fills the key frames with missing left and right hand properties.
   * If key frame 1 has property X and key frame 2 is missing X,
   * but key frame 3 has X again, then we need to assign X to key frame 2 too.
   *
   * @param sk A skrollable.
   */
  var _fillProps = function(sk) {
    //Will collect the properties key frame by key frame
    var propList = {};
    var keyFrameIndex;
    var keyFramesLength;

    //Iterate over all key frames from left to right
    keyFrameIndex = 0;
    keyFramesLength = sk.keyFrames.length;

    for (; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
      _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
    }

    //Now do the same from right to fill the last gaps

    propList = {};

    //Iterate over all key frames from right to left
    keyFrameIndex = sk.keyFrames.length - 1;

    for (; keyFrameIndex >= 0; keyFrameIndex--) {
      _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
    }
  };

  var _fillPropForFrame = function(frame, propList) {
    var key;

    //For each key frame iterate over all right hand properties and assign them,
    //but only if the current key frame doesn't have the property by itself
    for (key in propList) {
      //The current frame misses this property, so assign it.
      if (!hasProp.call(frame.props, key)) {
        frame.props[key] = propList[key];
      }
    }

    //Iterate over all props of the current frame and collect them
    for (key in frame.props) {
      propList[key] = frame.props[key];
    }
  };

  /**
   * Calculates the new values for two given values array.
   */
  var _calcInterpolation = function(val1, val2, progress) {
    var valueIndex;
    var val1Length = val1.length;

    //They both need to have the same length
    if (val1Length !== val2.length) {
      throw "Can't interpolate between \"" +
        val1[0] +
        '" and "' +
        val2[0] +
        '"';
    }

    //Add the format string as first element.
    var interpolated = [val1[0]];

    valueIndex = 1;

    for (; valueIndex < val1Length; valueIndex++) {
      //That's the line where the two numbers are actually interpolated.
      interpolated[valueIndex] =
        val1[valueIndex] + (val2[valueIndex] - val1[valueIndex]) * progress;
    }

    return interpolated;
  };

  /**
   * Interpolates the numeric values into the format string.
   */
  var _interpolateString = function(val) {
    var valueIndex = 1;

    rxInterpolateString.lastIndex = 0;

    return val[0].replace(rxInterpolateString, function() {
      return val[valueIndex++];
    });
  };

  /**
   * Resets the class and style attribute to what it was before skrollr manipulated the element.
   * Also remembers the values it had before reseting, in order to undo the reset.
   */
  var _reset = function(elements, undo) {
    //We accept a single element or an array of elements.
    elements = [].concat(elements);

    var skrollable;
    var element;
    var elementsIndex = 0;
    var elementsLength = elements.length;

    for (; elementsIndex < elementsLength; elementsIndex++) {
      element = elements[elementsIndex];
      skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]];

      //Couldn't find the skrollable for this DOM element.
      if (!skrollable) {
        continue;
      }

      if (undo) {
        //Reset class and style to the "dirty" (set by skrollr) values.
        element.style.cssText = skrollable.dirtyStyleAttr;
        _updateClass(element, skrollable.dirtyClassAttr);
      } else {
        //Remember the "dirty" (set by skrollr) class and style.
        skrollable.dirtyStyleAttr = element.style.cssText;
        skrollable.dirtyClassAttr = _getClass(element);

        //Reset class and style to what it originally was.
        element.style.cssText = skrollable.styleAttr;
        _updateClass(element, skrollable.classAttr);
      }
    }
  };

  /**
   * Detects support for 3d transforms by applying it to the skrollr-body.
   */
  var _detect3DTransforms = function() {
    _translateZ = "translateZ(0)";
    skrollr.setStyle(_skrollrBody, "transform", _translateZ);

    var computedStyle = getStyle(_skrollrBody);
    var computedTransform = computedStyle.getPropertyValue("transform");
    var computedTransformWithPrefix = computedStyle.getPropertyValue(
      theDashedCSSPrefix + "transform"
    );
    var has3D =
      (computedTransform && computedTransform !== "none") ||
      (computedTransformWithPrefix && computedTransformWithPrefix !== "none");

    if (!has3D) {
      _translateZ = "";
    }
  };

  /**
   * Set the CSS property on the given element. Sets prefixed properties as well.
   */
  skrollr.setStyle = function(el, prop, val) {
    var style = el.style;

    //Camel case.
    prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace("-", "");

    //Make sure z-index gets a <integer>.
    //This is the only <integer> case we need to handle.
    if (prop === "zIndex") {
      if (isNaN(val)) {
        //If it's not a number, don't touch it.
        //It could for example be "auto" (#351).
        style[prop] = val;
      } else {
        //Floor the number.
        style[prop] = "" + (val | 0);
      }
    }
    //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE.
    else if (prop === "float") {
      style.styleFloat = style.cssFloat = val;
    } else {
      //Need try-catch for old IE.
      try {
        //Set prefixed property if there's a prefix.
        if (theCSSPrefix) {
          style[
            theCSSPrefix + prop.slice(0, 1).toUpperCase() + prop.slice(1)
          ] = val;
        }

        //Set unprefixed.
        style[prop] = val;
      } catch (ignore) {}
    }
  };

  /**
   * Cross browser event handling.
   */
  var _addEvent = (skrollr.addEvent = function(element, names, callback) {
    var intermediate = function(e) {
      //Normalize IE event stuff.
      e = e || window.event;

      if (!e.target) {
        e.target = e.srcElement;
      }

      if (!e.preventDefault) {
        e.preventDefault = function() {
          e.returnValue = false;
        };
      }

      return callback.call(this, e);
    };

    names = names.split(" ");

    var name;
    var nameCounter = 0;
    var namesLength = names.length;

    for (; nameCounter < namesLength; nameCounter++) {
      name = names[nameCounter];

      if (element.addEventListener) {
        element.addEventListener(name, callback, false);
      } else {
        element.attachEvent("on" + name, intermediate);
      }

      //Remember the events to be able to flush them later.
      _registeredEvents.push({
        element: element,
        name: name,
        listener: callback
      });
    }
  });

  var _removeEvent = (skrollr.removeEvent = function(element, names, callback) {
    names = names.split(" ");

    var nameCounter = 0;
    var namesLength = names.length;

    for (; nameCounter < namesLength; nameCounter++) {
      if (element.removeEventListener) {
        element.removeEventListener(names[nameCounter], callback, false);
      } else {
        element.detachEvent("on" + names[nameCounter], callback);
      }
    }
  });

  var _removeAllEvents = function() {
    var eventData;
    var eventCounter = 0;
    var eventsLength = _registeredEvents.length;

    for (; eventCounter < eventsLength; eventCounter++) {
      eventData = _registeredEvents[eventCounter];

      _removeEvent(eventData.element, eventData.name, eventData.listener);
    }

    _registeredEvents = [];
  };

  var _reflow = function() {
    var pos = _instance.getScrollTop();

    //Will be recalculated by _updateDependentKeyFrames.
    _maxKeyFrame = 0;

    if (_forceHeight && !_isMobile) {
      //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216).
      body.style.height = "auto";
    }

    _updateDependentKeyFrames();

    if (_forceHeight && !_isMobile) {
      //"force" the height.
      body.style.height = _maxKeyFrame + documentElement.clientHeight + "px";
    }

    //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom).
    if (_isMobile) {
      _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame));
    } else {
      //Remember and reset the scroll pos (#217).
      _instance.setScrollTop(pos, true);
    }

    _forceRender = true;
  };

  /*
	 * Returns a copy of the constants object where all functions and strings have been evaluated.
	 */
  var _processConstants = function() {
    var viewportHeight = documentElement.clientHeight;
    var copy = {};
    var prop;
    var value;

    for (prop in _constants) {
      value = _constants[prop];

      if (typeof value === "function") {
        value = value.call(_instance);
      }
      //Percentage offset.
      else if (/p$/.test(value)) {
        value = (value.slice(0, -1) / 100) * viewportHeight;
      }

      copy[prop] = value;
    }

    return copy;
  };

  /*
	 * Returns the height of the document.
	 */
  var _getDocumentHeight = function() {
    var skrollrBodyHeight = (_skrollrBody && _skrollrBody.offsetHeight) || 0;
    var bodyHeight = Math.max(
      skrollrBodyHeight,
      body.scrollHeight,
      body.offsetHeight,
      documentElement.scrollHeight,
      documentElement.offsetHeight,
      documentElement.clientHeight
    );

    return bodyHeight - documentElement.clientHeight;
  };

  /**
   * Returns a string of space separated classnames for the current element.
   * Works with SVG as well.
   */
  var _getClass = function(element) {
    var prop = "className";

    //SVG support by using className.baseVal instead of just className.
    if (window.SVGElement && element instanceof window.SVGElement) {
      element = element[prop];
      prop = "baseVal";
    }

    return element[prop];
  };

  /**
   * Adds and removes a CSS classes.
   * Works with SVG as well.
   * add and remove are arrays of strings,
   * or if remove is ommited add is a string and overwrites all classes.
   */
  var _updateClass = function(element, add, remove) {
    var prop = "className";

    //SVG support by using className.baseVal instead of just className.
    if (window.SVGElement && element instanceof window.SVGElement) {
      element = element[prop];
      prop = "baseVal";
    }

    //When remove is ommited, we want to overwrite/set the classes.
    if (remove === undefined) {
      element[prop] = add;
      return;
    }

    //Cache current classes. We will work on a string before passing back to DOM.
    var val = element[prop];

    //All classes to be removed.
    var classRemoveIndex = 0;
    var removeLength = remove.length;

    for (; classRemoveIndex < removeLength; classRemoveIndex++) {
      val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), " ");
    }

    val = _trim(val);

    //All classes to be added.
    var classAddIndex = 0;
    var addLength = add.length;

    for (; classAddIndex < addLength; classAddIndex++) {
      //Only add if el not already has class.
      if (_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {
        val += " " + add[classAddIndex];
      }
    }

    element[prop] = _trim(val);
  };

  var _trim = function(a) {
    return a.replace(rxTrim, "");
  };

  /**
   * Adds a space before and after the string.
   */
  var _untrim = function(a) {
    return " " + a + " ";
  };

  var _now =
    Date.now ||
    function() {
      return +new Date();
    };

  var _keyFrameComparator = function(a, b) {
    return a.frame - b.frame;
  };

  /*
	 * Private variables.
	 */

  //Singleton
  var _instance;

  /*
		A list of all elements which should be animated associated with their the metadata.
		Exmaple skrollable with two key frames animating from 100px width to 20px:

		skrollable = {
			element: <the DOM element>,
			styleAttr: <style attribute of the element before skrollr>,
			classAttr: <class attribute of the element before skrollr>,
			keyFrames: [
				{
					frame: 100,
					props: {
						width: {
							value: ['{?}px', 100],
							easing: <reference to easing function>
						}
					},
					mode: "absolute"
				},
				{
					frame: 200,
					props: {
						width: {
							value: ['{?}px', 20],
							easing: <reference to easing function>
						}
					},
					mode: "absolute"
				}
			]
		};
	*/
  var _skrollables;

  var _skrollrBody;

  var _listeners;
  var _forceHeight;
  var _maxKeyFrame = 0;

  var _scale = 1;
  var _constants;

  var _mobileDeceleration;

  //Current direction (up/down).
  var _direction = "down";

  //The last top offset value. Needed to determine direction.
  var _lastTop = -1;

  //The last time we called the render method (doesn't mean we rendered!).
  var _lastRenderCall = _now();

  //For detecting if it actually resized (#271).
  var _lastViewportWidth = 0;
  var _lastViewportHeight = 0;

  var _requestReflow = false;

  //Will contain data about a running scrollbar animation, if any.
  var _scrollAnimation;

  var _smoothScrollingEnabled;

  var _smoothScrollingDuration;

  //Will contain settins for smooth scrolling if enabled.
  var _smoothScrolling;

  //Can be set by any operation/event to force rendering even if the scrollbar didn't move.
  var _forceRender;

  //Each skrollable gets an unique ID incremented for each skrollable.
  //The ID is the index in the _skrollables array.
  var _skrollableIdCounter = 0;

  var _edgeStrategy;

  //Mobile specific vars. Will be stripped by UglifyJS when not in use.
  var _isMobile = false;

  //The virtual scroll offset when using mobile scrolling.
  var _mobileOffset = 0;

  //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).
  var _translateZ;

  //Will contain data about registered events by skrollr.
  var _registeredEvents = [];

  //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).
  var _animFrame;
})(window, document);
